5.05. Справочник по LINQ
Справочник по LINQ
📌 Основные концепции и архитектура LINQ
1.1. Что такое LINQ?
LINQ (Language Integrated Query) — это единая модель запросов к данным в C#, реализованная через:
- Синтаксис выражений запросов (
from ... where ... select) - Методы расширения (
Where,Select,GroupBy, и др.) - Интерфейсы и обобщённые делегаты (
IEnumerable<T>,IQueryable<T>,Func<...>,Expression<...>) - Провайдеры LINQ, реализующие семантику выполнения (например:
Enumerable,Queryable,ParallelEnumerable,EntityFrameworkCore)
1.2. Два основных режима выполнения
| Режим | Интерфейс | Пространство имён | Исполнение | Тип делегатов |
|---|---|---|---|---|
| LINQ to Objects | IEnumerable<T> | System.Linq.Enumerable | Выполняется в .NET CLR. Сразу на материализованных данных. | Func<T, ...> |
| LINQ to Entities / IQueryable | IQueryable<T> | System.Linq.Queryable | Выражение анализируется провайдером (например, EF Core) и транслируется (в SQL, REST и т.п.). | Expression<Func<T, ...>> |
⚠️ Важно:
AsEnumerable()— переводитIQueryable<T>→IEnumerable<T>→ останавливает трансляцию провайдером, дальнейшие операции выполняются локально.AsQueryable()— оборачиваетIEnumerable<T>вIQueryable<T>, но если источник не поддерживаетIQueryable, то в итоге запрос всё равно будет выполнен черезEnumerable(с преобразованиемExpression→FuncвнутриQueryableчерезExpression.Compile()).
1.3. Архитектура провайдера LINQ
- Expression Tree (
Expression<TDelegate>) — неисполняемое дерево выражения; используется для интроспекции и трансляции.- Не может содержать:
- Локальные переменные (кроме как через замыкание — но это
ConstantExpressionс объектом замыкания), - Вызовы произвольных методов (если только провайдер не поддерживает их маппинг, например
EF.Functions.Like), - Конструкторы некоторых типов (
new MyClass()→NewExpression, но поддержка зависит от провайдера), - Некоторые лямбды со сложной логикой (например, рекурсия, try-catch, yield).
- Локальные переменные (кроме как через замыкание — но это
- Не может содержать:
- Провайдер (
IQueryProvider) — реализуетExecute<T>иCreateQuery<T>. Пример:EntityQueryProvider,InMemoryQueryProvider. - Источник (
IQueryable<T>.Expression,IQueryable<T>.Provider) — вместе определяют, что и как будет выполнено.
1.4. Фундаментальные делегаты и сигнатуры
Все стандартные LINQ-методы опираются на несколько шаблонных делегатов:
| Назначение | Тип делегата | Пример использования |
|---|---|---|
| Условие фильтрации | Func<TSource, bool> или Expression<Func<TSource, bool>> | Where(x => x.Id > 0) |
| Проекция (mapping) | Func<TSource, TResult> / Expression<Func<TSource, TResult>> | Select(x => x.Name) |
| Ключ группировки | Func<TSource, TKey> / Expression<Func<TSource, TKey>> | GroupBy(x => x.CategoryId) |
| Сравнение ключей | IEqualityComparer<TKey> | GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase) |
| Сортировка | IComparer<TKey> (в OrderBy через keySelector) | OrderBy(x => x.Date, Comparer<DateTime>.Default) |
| Аккумуляция | Func<TAccumulate, TSource, TAccumulate> | Aggregate(seed, (acc, x) => acc + x.Value) |
| Элемент + индекс | Func<TSource, int, TResult> | Select((item, i) => new { item, Index = i }) |
| Элементы двух последовательностей | Func<TFirst, TSecond, TResult> | Zip(xs, ys, (a, b) => a + b) |
Примечание: в
Queryable, если передатьFunc, он автоматически преобразуется вExpressionчерез замыкание, но не транслируется провайдером — а выполняется локально после материализации (что часто приводит к ошибкам или неэффективности).
1.5. Отложенное (deferred) vs Немедленное (immediate) выполнение
| Тип | Поведение | Примеры |
|---|---|---|
| Отложенное | Возвращает IEnumerable<T> / IQueryable<T>; логика ещё не выполнена. Создаётся итератор. | Where, Select, OrderBy, GroupBy, Join, Zip, Take, Skip, Distinct, Cast, OfType |
| Немедленное | Выполняет перечисление и возвращает материализованный результат или скаляр. | ToArray, ToList, ToDictionary, ToHashSet, Count, LongCount, Any, All, First, FirstOrDefault, Single, SingleOrDefault, Last, LastOrDefault, Min, Max, Sum, Average, Aggregate, ElementAt, Contains, SequenceEqual |
⚠️ Опасность: многократный вызов отложенного запроса — многократное выполнение. Всегда материализуйте, если результат нужен многократно.
Решение:var cached = query.ToList();
1.6. Потокобезопасность
IEnumerable<T>сам по себе — не потокобезопасен.- Несколько потоков не могут одновременно
MoveNext()по одному и тому жеIEnumerator<T>. IQueryable<T>— тоже не потокобезопасен (егоProviderиExpressionмогут быть изменяемыми в некоторых провайдерах).- Некоторые источники (например
ImmutableArray<T>,ReadOnlySpan<T>.ToArray().AsEnumerable()) безопасны для чтения, если данные не изменяются.
📌 Методы запросов (System.Linq.Enumerable / System.Linq.Queryable)
Все методы ниже имеют две реализации в .NET BCL:
public static IEnumerable<T> MethodName<T>(this IEnumerable<T> source, ...)→ вEnumerablepublic static IQueryable<T> MethodName<T>(this IQueryable<T> source, ...)→ вQueryableПри вызове компилятор разрешает перегрузку по типу
source.
Важно: сигнатуры параметров делегатов различаются:
- Для
Enumerable—Func<...>- Для
Queryable—Expression<Func<...>>(кроме случаев сIEqualityComparer<T>иIComparer<T>, которые передаются как есть).
2.1. Фильтрация
| Метод | Сигнатура (упрощённо) | Описание | Особенности |
|---|---|---|---|
Where<TSource> | Where(Func<TSource, bool> predicate) Where(Func<TSource, int, bool> predicate) | Фильтрует элементы по предикату. Вторая перегрузка передаёт индекс (от 0). | - Отложенное выполнение. - Может бросить ArgumentNullException, если source или predicate == null. - В Queryable — Expression анализируется и может быть транслирован (например, в WHERE SQL). |
OfType<TResult> | OfType<TResult>() | Фильтрует и преобразует элементы к указанному типу (включая null для ссылочных типов, если элемент несовместим). | - Не требует source как IEnumerable<T> — работает на IEnumerable (non-generic). - Использует is + as: эквивалент x => x is TResult ? (TResult)x : default. - Не бросает InvalidCastException. |
Cast<TResult> | Cast<TResult>() | Принудительно преобразует каждый элемент к TResult. | - Бросает InvalidCastException, если элемент несовместим. - Эквивалент x => (TResult)x. - Работает на IEnumerable. |
Take<TSource> | Take(int count) Take(Range range) (.NET 8+) | Возвращает первые count элементов. Range позволяет: Take(..3), Take(2..), Take(^3..). | - Отложенное. - count < 0 → ArgumentOutOfRangeException. |
Skip<TSource> | Skip(int count) Skip(Range range) (.NET 8+) | Пропускает первые count элементов. | Аналогично Take. |
TakeWhile<TSource> | TakeWhile(Func<TSource, bool> predicate) TakeWhile(Func<TSource, int, bool> predicate) | Берёт элементы пока предикат истинен. Остановка при первом false. | - Отложенное. - Не проверяет оставшиеся элементы после первой неудачи. |
SkipWhile<TSource> | SkipWhile(Func<TSource, bool> predicate) SkipWhile(Func<TSource, int, bool> predicate) | Пропускает элементы пока предикат истинен, затем возвращает оставшиеся. | Аналогично TakeWhile. |
Пример (индекс в
Where):var odds = numbers.Where((x, i) => i % 2 == 1); // элементы с нечётными индексами
Пример (Range в .NET 8):
var mid = list.Take(1..^1); // без первого и последнего
var lastThree = list.Take(^3..);
2.2. Проекция (Select, SelectMany)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
Select<TSource, TResult> | Select(Func<TSource, TResult> selector) Select(Func<TSource, int, TResult> selector) | Преобразует каждый элемент. Вторая перегрузка включает индекс. | - Отложенное. - Может проецировать в анонимные типы, кортежи, DTO. - В Queryable — Expression транслируется в SELECT (включая вычислимые поля). |
SelectMany<TSource, TCollection, TResult> | SelectMany(Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) SelectMany(Func<TSource, IEnumerable<TResult>> selector) | "Сглаживает" иерархию: из TSource → IEnumerable<TCollection> делает плоскую последовательность. Эквивалент from x in source from y in x.Items select y. | - Первая перегрузка: селектор коллекции + селектор результата (позволяет использовать оба значения). - Вторая перегрузка: сразу → TResult. - Используется для имитации JOIN или обхода коллекций внутри элементов (orders.SelectMany(o => o.Items)). - В Queryable — транслируется в CROSS APPLY (SQL Server) или LATERAL JOIN (PostgreSQL). |
Пример (связь с синтаксисом запроса):
// Методы:
customers.SelectMany(c => c.Orders, (c, o) => new { c.Name, o.Total });
// Запрос:
from c in customers
from o in c.Orders
select new { c.Name, o.Total };
⚠️ Внимание:
SelectManyне фильтруетnull. ЕслиcollectionSelectorвернётnull, будетInvalidOperationException("Value cannot be null").- Решение:
c => c.Orders ?? Enumerable.Empty<Order>().
2.3. Сортировка (OrderBy, ThenBy, Reverse)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
OrderBy<TSource, TKey> | OrderBy(Func<TSource, TKey> keySelector) OrderBy(Func<TSource, TKey> keySelector, IComparer<TKey> comparer) | Первичная сортировка по возрастанию. | - Отложенное. - Возвращает IOrderedEnumerable<TSource> / IOrderedQueryable<TSource> — тип, поддерживающий ThenBy. |
OrderByDescending | Аналогично | Сортировка по убыванию. | |
ThenBy<TSource, TKey> | ThenBy(Func<TSource, TKey> keySelector) ThenBy(Func<TSource, TKey> keySelector, IComparer<TKey> comparer) | Дополнительный уровень сортировки (для IOrderedEnumerable<T>). | Может быть вызван цепочкой: .OrderBy(...).ThenBy(...).ThenBy(...) |
ThenByDescending | Аналогично | Доп. сортировка по убыванию. | |
Reverse<TSource> | Reverse() | Меняет порядок элементов на обратный. | - Не сохраняет стабильность сортировки, если исходная последовательность неупорядочена. - В Enumerable реализован через буфер (материализует до конца при первом MoveNext). - Не используется в Queryable провайдерами напрямую — может вызвать клиентскую оценку. |
Пример (многоуровневая сортировка):
var sorted = people
.OrderBy(p => p.Department)
.ThenBy(p => p.Salary)
.ThenByDescending(p => p.HireDate);
О сравнении:
IComparer<TKey>позволяет задать:
- Пользовательский порядок (
new CustomComparer()),- Culture-aware сортировку (
StringComparer.Create(culture, ignoreCase)),Comparer<T>.Default(используетIComparable<T>илиIComparable).- Если
comparer == null, используетсяComparer<TKey>.Default.
2.4. Операции над множествами (Distinct, Union, Intersect, Except, Concat)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
Distinct<TSource> | Distinct() Distinct(IEqualityComparer<TSource> comparer) | Удаляет дубликаты. | - Отложенное (в Enumerable) → но первый вызов MoveNext() материализует ВСЕ элементы в HashSet. - Порядок: первое вхождение сохраняется. |
Union<TSource> | Union(IEnumerable<TSource> second) Union(IEnumerable<TSource> second, IEqualityComparer<TSource> comparer) | Объединение двух последовательностей без дубликатов. | - Аналог Distinct(source.Concat(second)), но оптимизирован (один проход). - Не сохраняет порядок second относительно source (элементы source идут первыми, затем новые из second). |
Intersect<TSource> | Intersect(IEnumerable<TSource> second, ...) | Пересечение: элементы, присутствующие в обеих последовательностях. | - В Enumerable: second материализуется в HashSet, затем фильтруется source. - Порядок — как в source. |
Except<TSource> | Except(IEnumerable<TSource> second, ...) | Разность: элементы из source, отсутствующие в second. | Аналогично Intersect. |
Concat<TSource> | Concat(IEnumerable<TSource> second) | Простое объединение (с дубликатами). | - Отложенное, потоковое: не материализует source или second заранее. - Может соединять бесконечные последовательности (Enumerable.Repeat(1).Concat(Enumerable.Repeat(2))). |
Append<TSource> | Append(TSource element) | Добавляет один элемент в конец. | Эквивалент source.Concat(Enumerable.Repeat(element, 1)), но эффективнее. |
Prepend<TSource> | Prepend(TSource element) | Добавляет один элемент в начало. |
⚠️ Производительность:
Union/Intersect/Exceptтребуют O(n + m) времени и O(min(n, m)) памяти (дляHashSet).- Избегайте вызова
Distinct()послеUnion/Intersect/Except— они уже возвращают уникальные элементы.
2.5. Агрегация (Count, LongCount, Min, Max, Sum, Average, Aggregate)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
Count<TSource> | Count() Count(Func<TSource, bool> predicate) | Количество элементов (или удовлетворяющих условию). | - Немедленное. - Для ICollection<T> / ICollection использует .Count напрямую (O(1)). - Для остальных — перечисление (O(n)). |
LongCount<TSource> | Аналогично | Возвращает long. Используйте при ожидании > 2 млрд элементов. | |
Any<TSource> | Any() Any(Func<TSource, bool> predicate) | Проверка наличия хотя бы одного элемента (или удовлетворяющего условию). | - Для ICollection<T> — проверка Count > 0. - Иначе — MoveNext() один раз → очень эффективно. |
All<TSource> | All(Func<TSource, bool> predicate) | Все элементы удовлетворяют предикату? | - Остановка при первом false. |
Min<TSource> | Min() Min<TResult>(Func<TSource, TResult> selector) | Минимальный элемент или значение проекции. | - Для пустой последовательности: |
•
Min()наint/double/DateTime→InvalidOperationException
•Min()наint?/double?→null(начиная с .NET 6)
•MinBy()(см. ниже) безопаснее для объектов. |
|Max<TSource>| Аналогично | | |
|MinBy<TSource, TKey>|MinBy(Func<TSource, TKey> keySelector)MinBy(..., IComparer<TKey> comparer)(.NET 6+) | Возвращает элемент, имеющий минимальное значение ключа. | - Для пустой последовательности →InvalidOperationException.
- Может вернуть любой из элементов с минимальным ключом (не гарантирован первый). |
|MaxBy<TSource, TKey>| Аналогично | | |
|Sum<TSource>|Sum(Func<TSource, int>)(и дляlong,float,double,decimal) | Сумма проекций. | - На пустой последовательности →0(всегда). |
|Average<TSource>| АналогичноSum, но возвращаетdouble/float| Среднее значение. | - Пустая последовательность →InvalidOperationException. |
|Aggregate<TSource>|Aggregate(Func<TSource, TSource, TSource> func)Aggregate<TAccumulate>(TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)Aggregate<TAccumulate, TResult>(seed, func, resultSelector)| Обобщённая свёртка. | - Первая перегрузка: первый элемент — seed, затемfunc(acc, next). Пустая послед-ть → исключение.- Вторая: явный
seed. Пустая → возвращаетseed.- Третья: позволяет преобразовать аккумулятор в результат (например,
(count, sum) → avg = sum / count). |
Пример (
Aggregateдля среднего безAverage):var avg = list.Aggregate(
seed: (Count: 0, Sum: 0.0),
func: (acc, x) => (acc.Count + 1, acc.Sum + x),
resultSelector: acc => acc.Count == 0 ? 0.0 : acc.Sum / acc.Count
);
2.6. Группировка (GroupBy, ToLookup)
| Метод | Сигнатура (упрощённо) | Описание | Особенности |
|---|---|---|---|
GroupBy<TSource, TKey> | GroupBy(Func<TSource, TKey> keySelector) GroupBy(keySelector, IEqualityComparer<TKey> comparer) | Группирует элементы по ключу. Возвращает IEnumerable<IGrouping<TKey, TSource>>. | - Отложенное. - Каждая группа — это IGrouping<TKey, T>: реализует IEnumerable<T> + имеет свойство Key. - Ключи вычисляются один раз на элемент. - Порядок групп — порядок первого вхождения ключа в source. |
GroupBy<TSource, TKey, TElement> | GroupBy(keySelector, elementSelector) GroupBy(keySelector, elementSelector, comparer) | Позволяет преобразовать элементы внутри групп (проекция). | Эквивалент: source.GroupBy(k => k.Key).Select(g => new { g.Key, Elements = g.Select(e => e.Projection) }) |
GroupBy<TSource, TKey, TResult> | GroupBy(keySelector, resultSelector) GroupBy(keySelector, elementSelector, resultSelector) GroupBy(..., comparer) | Вместо IGrouping возвращает кастомный результат на группу (например, агрегат). | - resultSelector: Func<TKey, IEnumerable<TElement>, TResult> - Позволяет делать агрегацию внутри групп без дополнительного Select: GroupBy(x => x.Category, (key, items) => new { Category = key, Count = items.Count() }) |
ToLookup<TSource, TKey> | ToLookup(Func<TSource, TKey> keySelector) ToLookup(keySelector, elementSelector) ToLookup(..., comparer) | Создаёт неизменяемый ILookup<TKey, TElement> — словарь групп. | - Немедленное выполнение (материализует все данные). - ILookup — похож на Dictionary<TKey, IEnumerable<T>>, но: • всегда возвращает IEnumerable<T> (даже для отсутствующего ключа — пустая последовательность), • неизменяем, • допускает дубликаты ключей (в отличие от Dictionary). - Подходит для многократных запросов по ключам. |
Пример (
GroupBy→Dictionary):var dict = people
.GroupBy(p => p.Department)
.ToDictionary(g => g.Key, g => g.ToList());Пример (
ToLookupдля многократного доступа):var lookup = orders.ToLookup(o => o.CustomerId);
foreach (var id in customerIds)
{
var ordersForCustomer = lookup[id]; // всегда IEnumerable<Order>, даже если 0
}
⚠️ Важно:
GroupByвQueryable(EF Core) генерирует SQLGROUP BY, но если после него использовать методы вродеCount(),Sum(),Min()без проекции черезSelect, EF может выполнить запрос и агрегацию на клиенте (N+1 или materialization warning).- Безопасно:
.GroupBy(x => x.Category)
.Select(g => new { g.Key, Count = g.Count() }) // → SELECT Category, COUNT(*) GROUP BY Category
2.7. Соединения (Join, GroupJoin)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
Join<TOuter, TInner, TKey, TResult> | Join(IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null) | Эквивалент SQL INNER JOIN. | - Отложенное. - Реализация в Enumerable: материализует inner в Lookup<TKey, TInner>, затем для каждого outer ищет совпадения. - Порядок: как в outer. - Повторяющиеся ключи в inner → несколько результатов на один outer. |
GroupJoin<TOuter, TInner, TKey, TResult> | GroupJoin(..., Func<TOuter, IEnumerable<TInner>, TResult> resultSelector, ...) | Эквивалент SQL LEFT JOIN + группировка правой части. | - Для каждого outer возвращает IEnumerable<TInner> (возможно, пустой). - Основа для реализации LEFT JOIN: csharp outer.GroupJoin(inner, o => o.Id, i => i.OuterId, (o, inners) => new { o, Inners = inners.DefaultIfEmpty() }) |
Пример (
LEFT JOINчерезGroupJoin+SelectMany):var leftJoin = customers
.GroupJoin(orders, c => c.Id, o => o.CustomerId,
(c, os) => new { Customer = c, Orders = os })
.SelectMany(x => x.Orders.DefaultIfEmpty(),
(x, o) => new { x.Customer.Name, OrderDate = o?.Date });Это даёт плоский результат, как в SQL
LEFT JOIN.
⚠️ В
Queryable:
Join→INNER JOINGroupJoin→LEFT JOIN(в EF Core 5+)- Сложные условия (например,
ON a.X = b.Y AND a.Z > 5) требуютWhereпослеJoin, либо использованияSelectMany+Where.
2.8. Элементы и позиции (First, Last, Single, ElementAt, и др.)
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
First<TSource> | First() First(Func<TSource, bool> predicate) | Первый элемент (или первый удовлетворяющий условию). | - Немедленное. - Пустая послед-ть / нет совпадений → InvalidOperationException. |
FirstOrDefault<TSource> | Аналогично | То же, но возвращает default(T) при отсутствии. | - Для ссылочных типов и Nullable<T> → null. |
Last<TSource> | Last() Last(Func<TSource, bool> predicate) | Последний элемент (или последний по условию). | - В Enumerable: если source не IList<T>, материализует ВСЁ (O(n), O(n) памяти). - Избегайте на больших или бесконечных послед-тях. |
LastOrDefault<TSource> | Аналогично | Возвращает default(T) при отсутствии. | |
Single<TSource> | Single() Single(predicate) | Ровно один элемент (или один по условию). | - Проверяет, что элементов ровно один. 0 или ≥2 → InvalidOperationException. - Безопасен для получения уникального результата (например, по PK). |
SingleOrDefault<TSource> | Аналогично | Возвращает default(T), если 0 элементов. | |
ElementAt<TSource> | ElementAt(int index) | Элемент по индексу. | - Для IList<T> → O(1) (list[index]). - Иначе — перебор до index → O(n). - index < 0 или ≥ Count → ArgumentOutOfRangeException. |
ElementAtOrDefault<TSource> | Аналогично | Возвращает default(T) при выходе за границы. | |
DefaultIfEmpty<TSource> | DefaultIfEmpty() DefaultIfEmpty(TSource defaultValue) | Возвращает последовательность из одного default(T) / defaultValue, если source пуста. | - Отложенное. - Используется для эмуляции LEFT JOIN, когда правая часть может быть пустой. |
SingleOrDefault + ?? throw | — | Паттерн «найти или исключение»: | csharp var item = list.SingleOrDefault(x => x.Id == id) ?? throw new NotFoundException(); |
Пример (безопасный
LastдляIQueryable):// Плохо (может привести к клиентской оценке):
var last = context.Orders.OrderBy(o => o.Date).Last();
// Лучше:
var last = context.Orders.OrderByDescending(o => o.Date).FirstOrDefault();
2.9. Генерация и преобразование последовательностей
| Метод | Сигнатура | Описание | Особенности |
|---|---|---|---|
Empty<TSource>() | Empty<T>() | Возвращает пустую IEnumerable<T>. | - Singleton-реализация (один и тот же экземпляр T[0]). - Используется для инициализации, объединения, fallback. |
Range(int start, int count) | Range(0, 10) | Последовательность целых: start, start+1, ..., start+count-1. | - count < 0 → ArgumentOutOfRangeException. - Может использоваться для генерации индексов. |
Repeat<TSource>(TSource element, int count) | Repeat("x", 3) | Повторяет элемент count раз. | - count < 0 → исключение. - count == 0 → пустая послед-ть. |
Zip<TFirst, TSecond, TResult> | Zip(IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector) (.NET Core 3+) Zip(first, second) → возвращает IEnumerable<(TFirst, TSecond)> (.NET 6+) | Параллельное объединение по позиции («молния»). | - Останавливается по короткой последовательности. - В .NET 6+ перегрузка без селектора возвращает кортежи. - Аналог Python zip(). |
Chunk<TSource>(int size) | Chunk(100) (.NET 6+) | Разбивает последовательность на чанки (массивы) указанного размера. | - Последний чанк может быть короче. - Немедленное (материализует чанк при запросе). - Используется для пакетной обработки (bulk-операции в БД). |
TakeLast<TSource>(int count) | TakeLast(5) (.NET 6+) | Последние count элементов. | - В Enumerable: материализует буфер размера count (O(count) памяти), один проход. - Не эквивалент Reverse().Take(count).Reverse() (тот требует O(n) памяти). |
SkipLast<TSource>(int count) | SkipLast(5) (.NET 6+) | Все элементы, кроме последних count. | Аналогично TakeLast — эффективен (буфер фикс. размера). |
DistinctBy<TSource, TKey> | DistinctBy(x => x.Id) (.NET 6+) | Удаление дубликатов по ключу. | - Аналог GroupBy(key).Select(g => g.First()), но оптимизирован (один проход, HashSet<TKey>). - Безопасен для больших объектов (не хранит сами элементы, только ключи). |
UnionBy, IntersectBy, ExceptBy | UnionBy(second, x => x.Id) (.NET 7+) | Версии Union/Intersect/Except с селектором ключа. | - Позволяют сравнивать по свойству, не реализуя IEquatable<T>. - Эффективны (используют HashSet<TKey>). |
Пример (
Chunkдля пакетной вставки):foreach (var chunk in items.Chunk(1000))
{
context.Orders.AddRange(chunk);
await context.SaveChangesAsync();
}
Пример (
Zipс кортежами в .NET 6+):var pairs = names.Zip(ages); // IEnumerable<(string, int)>
foreach (var (name, age) in pairs) { ... }
📌 Свёртка, итерация, и расширенные методы из System.Interactive
Важно: Методы из
System.Interactive(пакетSystem.Interactive) не входят в стандартную поставку .NET, но широко используются в продвинутых сценариях (потоковая обработка, реактивное программирование, сложные трансформации). Ниже — только те, что имеют практическую ценность для справочника и не дублируютEnumerable.
4.1. Свёртка и поэлементная аккумуляция
| Метод | Из | Сигнатура | Описание | Пример использования |
|---|---|---|---|---|
Aggregate<TSource> | Enumerable | — | Уже рассмотрен в части 2.5. | — |
Scan<TSource> | System.Interactive | Scan(Func<TSource, TSource, TSource> accumulator) Scan<TAccumulate>(seed, Func<TAccumulate, TSource, TAccumulate> accumulator) | Возвращает промежуточные состояния свёртки (в отличие от Aggregate, который возвращает только конечный результат). | csharp var sums = new[] {1,2,3}.Scan(0, (acc, x) => acc + x); // [1,3,6] |
ScanBy<TSource, TKey> | System.Interactive.Async (редко) | — | Аналогично, но по ключу (редко используется). | — |
⚠️
Scan— отложенное, но требует хранения состояния между элементами → не thread-safe.
Полезно для: кумулятивных сумм, скользящих окон (в комбинации сSkip,Take), отладки пайплайнов.
4.2. Последовательности с окнами и сдвигами
| Метод | Из | Сигнатура | Описание | Особенности |
|---|---|---|---|---|
Buffer<TSource>(int count) | System.Interactive | Buffer(3) | Группирует элементы по count, возвращает IEnumerable<IList<TSource>>. Последняя группа может быть неполной. | Эквивалент Chunk, но возвращает IList<T>, а не T[]. |
Buffer<TSource>(int count, int skip) | System.Interactive | Buffer(3, 1) | Скользящее окно: сдвиг на skip, размер окна count. | csharp [1,2,3,4].Buffer(2,1) → [[1,2],[2,3],[3,4]] |
Pairwise<TSource> | System.Interactive | Pairwise() | Возвращает (current, next) для соседних элементов. | csharp [1,2,3].Pairwise() → [(1,2), (2,3)] - Пустая или одиночная послед-ть → пустой результат. |
StartWith<TSource>(params TSource[]) | System.Interactive | StartWith(0) | Добавляет элементы в начало последовательности. | Эквивалент new[] { x }.Concat(source), но чище. |
EndWith<TSource> | System.Interactive | EndWith(999) | Добавляет в конец. | Эквивалент source.Append(x), но принимает params. |
4.3. Условная и отложенная логика
| Метод | Из | Сигнатура | Описание |
|---|---|---|---|
If<TSource>(bool condition, Func<IEnumerable<TSource>> thenSource) | System.Interactive | — | Условное включение последовательности. |
Switch<TSource>(Func<int, IEnumerable<TSource>> selector) | System.Interactive | — | Выбор источника по индексу (редко). |
Defer<TSource>(Func<IEnumerable<TSource>> factory) | System.Interactive | — | Откладывает создание источника до первого MoveNext(). Полезно для отложенной инициализации (например, чтение из файла только при использовании). |
Пример (
Deferдля ленивой инициализации):var lazyLines = Defer(() => File.ReadLines("log.txt"));
// Файл не открыт, пока не начнётся перечисление.
4.4. Итерация без материализации (ForEach — не метод LINQ!)
⚠️ Важно:
.ForEach()отсутствует вIEnumerable<T>в стандартной библиотеке.- Он есть у
List<T>, но это метод экземпляра, не расширение LINQ.- Попытка написать
source.ForEach(Console.WriteLine)не скомпилируется, еслиsourceнеList<T>.
Рекомендуемый подход:
foreach (var item in source)
{
Console.WriteLine(item);
}
Если нужно в цепочке (например, для логгирования):
public static IEnumerable<T> Inspect<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
{
action(item);
yield return item;
}
}
// Использование:
var result = data
.Where(x => x > 0)
.Inspect(x => Console.WriteLine($"Filtered: {x}"))
.Select(x => x * 2);
Такой метод безопасен и сохраняет отложенность.
📌 Особенности IQueryable<T> и Expression Trees
5.1. Структура Expression<TDelegate>
Expression<Func<T, bool>> — это не делегат, а дерево объектов, описывающее код:
Expression<Func<int, bool>> expr = x => x > 5;
// Структура:
// Lambda
// └─ Body: BinaryExpression (GreaterThan)
// ├─ Left: ParameterExpression (x)
// └─ Right: ConstantExpression (5)
Поддерживаемые узлы (основные):
ParameterExpression— параметры (x)ConstantExpression— константы (5,"test",DateTime.Now— но вычисляется при построении!)MemberExpression— свойства/поля (x.Name,x.Age)MethodCallExpression— вызовы (x.ToString(),EF.Functions.Like(...))NewExpression—new MyClass(...)NewArrayExpression—new[] { x, y }ConditionalExpression—a ? b : cUnaryExpression—!,-,Convert,QuoteBinaryExpression—+,-,==,&&,||,<,>, и т.д.
Запрещено в Expression для провайдеров (например, EF Core):
- Локальные переменные (за исключением как
ConstantExpressionчерез замыкание — но значение "захватывается" при построении). - Методы, не имеющие маппинга (например,
x.Name.ToUpper()→ в EF Core 5+ работает, но в EF6 — нет;x.Name.Trim()→ зависит от провайдера). - Конструкторы с логикой (только инициализация полей/свойств).
Invokeна другихExpression(редко поддерживается).Block,Loop,TryCatch— не поддерживаются в деревьях выражений уровня 1 (только в Expression Trees v2, но не вExpression<T>).
5.2. Параметризация и замыкания
var threshold = 10;
var query = db.Orders.Where(o => o.Total > threshold);
Компилятор преобразует это в:
var captured = new { threshold };
Expression<Func<Order, bool>> = o => o.Total > captured.threshold;
Провайдер (например, EF Core) анализирует captured.threshold и параметризует запрос:
SELECT * FROM Orders WHERE Total > @__threshold_0
✅ Безопасно от SQL-инъекций.
❌ Но: если threshold — результат вычисления, оно выполняется на клиенте при построении выражения.
5.3. Компиляция выражений
Expression.Compile()→Func<T>(выполняется в CLR)Expression.Compile(true)→ интерпретируемая версия (медленнее, но меньше накладных расходов на JIT)Expression.CompileToMethod()→ генерация вDynamicMethod(устаревшее)
Используется внутри
Queryable, когдаIQueryableоборачиваетIEnumerable(например,AsQueryable()над списком).
5.4. Отладка Expression
expr.ToString()→"x => (x.Total > 10)"expr.DebugView(в отладчике VS) → подробное дерево- Библиотеки:
ExpressionTreeToString,LinqKit(ToString()улучшенный)
📌 Настройки и параметры выполнения (в т.ч. EF Core)
| Категория | Метод / Параметр | Применение | Описание |
|---|---|---|---|
| Отслеживание | AsNoTracking() | IQueryable<T> | Отключает change tracking → +производительность, −возможность SaveChanges(). |
AsTracking() / AsTracking(QueryTrackingBehavior) | Переключает обратно; TrackAll, TrackOnlyRoot. | ||
| Разделение запросов | AsSplitQuery() | EF Core 5+ | Генерирует несколько SQL-запросов вместо JOIN → избегает cartesian explosion. |
AsSingleQuery() | Возвращает поведение по умолчанию (JOIN). | ||
| Тегирование | TagWith(string) | EF Core 5+ | Добавляет комментарий в SQL → упрощает поиск в логах. |
TagWithCallSite() | Автоматически добавляет путь к файлу/строке. | ||
ToQueryString() | Получить SQL-запрос до выполнения (для отладки). | ||
| Отмена | WithCancellation(CancellationToken) | EF Core 6+ | Передаёт токен в ExecuteReaderAsync и т.д. |
| Поведение при ошибках | IgnoreQueryFilters() | Игнорирует глобальные фильтры (HasQueryFilter). | |
IgnoreAutoIncludes() | EF Core 5+ | Игнорирует AutoInclude. | |
| Материализация | AsEnumerable() | Переключает на LINQ to Objects → останавливает трансляцию в SQL. | |
AsAsyncEnumerable() | EF Core 6+ | Асинхронная версия для IAsyncEnumerable<T>. | |
| Специфичные функции | EF.Functions.Like() | WHERE Name LIKE '%test%' | |
EF.Functions.Collate() | Задаёт collation в запросе. | ||
EF.Property<T>(obj, "PropertyName") | Доступ к теневым свойствам. |
Пример комплексного запроса:
var orders = context.Orders
.TagWith("Dashboard: RecentOrders")
.Where(o => o.Date >= DateTime.UtcNow.AddDays(-7))
.Include(o => o.Customer)
.AsSplitQuery()
.AsNoTracking()
.OrderByDescending(o => o.Date)
.Take(100)
.ToList();
📌 Типичные ловушки и рекомендации по производительности
7.1. Общие ошибки и антипаттерны
| № | Проблема | Пример | Риск | Решение |
|---|---|---|---|---|
| 1 | N+1 запросов | orders.Select(o => new { o.Id, Customer = db.Customers.Find(o.CustomerId) }) | Сетевой оверхед, таймауты | Использовать Include, Join, или Select с проекцией до ToList() |
| 2 | Клиентская оценка (Client Evaluation) | .Where(o => o.Total.ToString().Contains("9")) | Полная загрузка таблицы в память | Избегать .ToString(), .ToLower() на стороне клиента; использовать EF.Functions.Like(o.Total.ToString(), "%9%") если провайдер поддерживает, или переписать на `o.Total % 10 == 9 |
| 3 | Материализация в середине цепочки | db.Orders.ToList().Where(o => o.Total > 100) | Загрузка всей таблицы | Убрать ToList() до Where |
| 4 | Многократное выполнение отложенного запроса | var q = db.Orders.Where(...); var c1 = q.Count(); var c2 = q.Average(o => o.Total); | Два одинаковых запроса к БД | Материализовать: var list = q.ToList(); |
| 5 | Использование First() вместо FirstOrDefault() для optional данных | db.Users.First(u => u.Email == email) при отсутствии → исключение | Crash при валидном сценарии | Использовать FirstOrDefault() ?? throw, если семантика «найти или ошибка» явно нужна |
| 6 | Last() на IQueryable<T> без OrderBy | db.Orders.Last() | Неопределённое поведение (SQL без ORDER BY + TOP 1) | Всегда: OrderBy(...).Last() → но лучше OrderByDescending(...).First() |
| 7 | Захват изменяемых переменных в замыкании | csharp<br>var filters = new List<Expression<Func<Order, bool>>>();<br>for (int i = 0; i < 3; i++)<br> filters.Add(o => o.Id == i);<br> | Все условия будут o.Id == 3 | Копировать в локальную переменную: var j = i; filters.Add(o => o.Id == j); |
| 8 | GroupBy → ToList() без проекции | db.Orders.GroupBy(o => o.CustomerId).ToList() | EF пытается загрузить группу как IGrouping<int, Order> → клиентская оценка | Проектировать сразу: .Select(g => new { g.Key, Count = g.Count() }) |
7.2. Производительность: время и память
| Метод | Время (среднее) | Память | Комментарий |
|---|---|---|---|
Count() на ICollection<T> | O(1) | O(1) | Использует свойство Count |
Count() на IEnumerable<T> | O(n) | O(1) | Перечисление |
Any() | O(1) | O(1) | MoveNext() один раз |
ToList() / ToArray() | O(n) | O(n) | Аллокация массива |
Distinct() / Union() / Intersect() | O(n + m) | O(min(n, m)) | HashSet |
OrderBy() | O(n log n) | O(n) | Стабильная сортировка (Timsort / IntroSort) |
GroupBy() (Enumerable) | O(n) | O(n) | Lookup<TKey, T> — словарь списков |
GroupBy() (Queryable) | Зависит от SQL | — | GROUP BY с индексом → быстро |
Skip(n).Take(m) на IQueryable<T> | O(1) в SQL (OFFSET FETCH) | — | Но без ORDER BY — нестабильно |
ElementAt(n) на IList<T> | O(1) | O(1) | Индексатор |
ElementAt(n) на IEnumerable<T> | O(n) | O(1) | Перебор |
Last() на IEnumerable<T> | O(n) | O(1) или O(n) | Если не IList<T> — буферизация всего (в текущей реализации — нет, но проход до конца с запоминанием последнего → O(1) памяти, O(n) времени) ✅ |
Reverse() | O(n) | O(n) | Буферизация |
✅ Начиная с .NET Core 3.0,
Last()наIEnumerable<T>не буферизует всю последовательность, а просто проходит итератор, сохраняя текущий элемент — память O(1).
⚠️ НоLast(predicate)всё ещё требует O(n) времени и O(1) памяти — корректно.
7.3. Рекомендации по оптимизации
- Индексируйте поля, используемые в
Where,OrderBy,GroupBy,Join. - Всегда используйте проекции (
Select) доToList(), чтобы передавать по сети только нужные столбцы. - Избегайте
Select(x => new { x, x.Relation })безInclude— приведёт к N+1 или ошибке. - Для пагинации:
.OrderBy(x => x.Id) // обязательно!
.Skip(pageSize * (page - 1))
.Take(pageSize) - Используйте
AsNoTracking()для read-only сценариев (отчёты, API-ответы). - При работе с большими объёмами —
Chunk()или курсоры (EF Core:ExecuteSqlRaw+DbDataReader). - Не используйте
ToList().ForEach(...)— это материализация + side effects в LINQ-стиле. - Избегайте
asyncвнутриSelect/Where— они неawaitятся. ИспользуйтеSelectAwaitизSystem.Linq.Async, если нужна асинхронная проекция.
📌 LINQ в асинхронном контексте
8.1. Асинхронные методы в EF Core (и других провайдерах)
| Синхронный | Асинхронный | Примечание |
|---|---|---|
ToList() | ToListAsync(ct) | — |
ToArray() | ToArrayAsync(ct) | — |
Count() | CountAsync(ct) | — |
Any() | AnyAsync(ct) | — |
First() | FirstAsync(ct) | — |
Single() | SingleAsync(ct) | — |
ForEachAsync(Action<T>) | ForEachAsync(ct) | Расширение EF Core: перечисляет и применяет действие (не возвращает значение) |
⚠️ Нет асинхронных версий для:
Where,Select,OrderByи других отложенных операций — они не выполняют IO, только строят выражение.- Асинхронность начинается только с материализующих методов.
8.2. IAsyncEnumerable<T> (.NET Core 3.0+)
Позволяет потоковую обработку результатов без полной материализации:
await foreach (var order in context.Orders
.Where(o => o.Total > 1000)
.AsAsyncEnumerable())
{
await ProcessAsync(order);
}
Преимущества:
- Память O(1) (один элемент за раз).
- Совместимо с
yield returnв кастомных асинхронных источниках. - Поддерживается EF Core 6+, Npgsql, MongoDB.Driver.
Ограничения:
- Нельзя использовать
Skip,Take,CountвнутриIAsyncEnumerable(без материализации). - Не все провайдеры поддерживают серверную пагинацию в потоке (EF Core — да, через курсоры в SQL Server 2012+).
8.3. Библиотека System.Linq.Async
Пакет System.Linq.Async (от Reactive Extensions) добавляет асинхронные аналоги LINQ:
| Метод | Описание |
|---|---|
WhereAwait, SelectAwait, OrderByAwait | Асинхронные предикаты/селекторы (Func<T, Task<bool>>) |
ToAsyncEnumerable() | Конвертирует IEnumerable<Task<T>> → IAsyncEnumerable<T> |
Merge() | Параллельное выполнение асинхронных операций |
Пример:
var results = await urls
.ToAsyncEnumerable()
.SelectAwait(async url => await httpClient.GetStringAsync(url))
.WhereAwait(async html => await IsRelevantAsync(html))
.ToListAsync();
⚠️ Используйте осторожно: асинхронные делегаты в LINQ нарушают принцип «чистых функций» и усложняют композицию. Предпочтительнее — сначала получить данные, потом обработать асинхронно.
📌 LINQ для нестандартных источников
9.1. LINQ to XML (System.Xml.Linq)
Работает с XDocument, XElement, XAttribute.
| Метод / Конструкция | Описание | Пример |
|---|---|---|
XDocument.Load("file.xml") | Загрузка XML | — |
doc.Descendants("book") | Все элементы <book> на любом уровне | — |
elem.Elements("author") | Дочерние <author> | — |
elem.Attributes("id") | Атрибуты id | — |
elem.Value | Текстовое содержимое | — |
elem.Attribute("id")?.Value | Значение атрибута | — |
new XElement("book", new XAttribute("id", 1), "Title") | Конструирование | — |
LINQ-запросы:
var expensiveBooks = doc
.Descendants("book")
.Where(b => (decimal)b.Element("price") > 30)
.Select(b => new {
Id = (int)b.Attribute("id"),
Title = b.Element("title").Value,
Price = (decimal)b.Element("price")
});
⚠️ Безопасность: используйте явные приведения (
(string),(int?)) — они возвращаютnullпри отсутствии элемента/атрибута. Прямой.Valueнаnull→NullReferenceException.
9.2. LINQ to JSON (через System.Text.Json + JsonDocument)
Нет встроенного IQueryable, но можно комбинировать с Select, Where:
using var doc = JsonDocument.Parse(json);
var items = doc.RootElement
.EnumerateArray()
.Where(e => e.TryGetProperty("price", out var p) && p.GetDouble() > 100)
.Select(e => new {
Name = e.GetProperty("name").GetString(),
Price = e.GetProperty("price").GetDouble()
});
Альтернатива: библиотеки вроде Newtonsoft.Json.Linq (JArray, JObject):
var jArray = JArray.Parse(json);
var expensive = jArray
.Where(t => t["price"]?.Value<decimal>() > 100)
.Select(t => new { Name = t["name"]?.ToString(), Price = t["price"]?.Value<decimal>() });
⚠️ Производительность:
JsonDocument— быстрее и безопаснее по памяти (IDisposable),JObject— удобнее для мутаций.
9.3. LINQ к Parquet, CSV, Avro (через Microsoft.Data.Analysis, CsvHelper, Parquet.Net)
Нет прямой поддержки IQueryable, но можно:
-
Прочитать в
IEnumerable<T>(лениво):var records = csvReader.GetRecords<Order>();
var filtered = records.Where(o => o.Total > 1000); -
Или загрузить в
DataFrame(Microsoft.Data.Analysis) и использоватьFilter,Select,GroupBy— но это не LINQ, а методы DataFrame API. -
Для Parquet:
using var parquetReader = await ParquetReader.CreateFromFileStreamAsync(stream);
var group = await parquetReader.ReadNextRowGroupAsync();
var column = group.Columns[0].Data.Cast<int>();
var query = column.Where(x => x > 100);
Вывод: для columnar-форматов (Parquet) LINQ применяется после извлечения колонок как
IEnumerable<T>.
9.4. Пользовательские IQueryable-провайдеры (кратко)
Можно реализовать свой провайдер:
- Реализовать
IQueryable<T>иIQueryProvider. - В
Execute— анализироватьExpression, генерировать запрос (SQL, REST, gRPC), выполнять, маппить. - Примеры:
Elasticsearch.Net(через NEST — частично),MongoDB.Driver(Find(), но неIQueryableпо умолчанию; естьAsQueryable()),OData-клиенты (Microsoft.OData.Client→DataServiceQuery<T>реализуетIQueryable<T>).
Пример (MongoDB):
var collection = db.GetCollection<Order>("orders");
var query = collection.AsQueryable()
.Where(o => o.CustomerId == 123 && o.Total > 1000)
.OrderByDescending(o => o.Date)
.Take(10);
// → транслируется в MongoDB find + sort + limit
⚠️ Не все операторы поддерживаются:
GroupBy,Join,SkipбезOrderByмогут вызвать клиентскую оценку.
📌 Параллельные LINQ — ParallelEnumerable
Пространство имён: System.Linq. Требует using System.Linq; (да, то же, но методы в ParallelEnumerable).
Синхронный (Enumerable) | Параллельный (ParallelEnumerable) | Условия эффективности |
|---|---|---|
AsParallel() | — | Начало PLINQ-цепочки |
Where | Where | CPU-bound, > 1000 элементов, функция не блокирует |
Select | Select | То же |
OrderBy | OrderBy | Дорого: O(n log n) + merge; лучше AsOrdered() после |
Aggregate | Aggregate(seedFactory, updateAccumulator, mergeAccumulators, resultSelector) | Требует ассоциативной и идемпотентной операции |
Особенности:
AsParallel()→ParallelQuery<T>AsSequential()→ возврат кIEnumerable<T>AsOrdered()— сохраняет порядок (снижает производительность)WithDegreeOfParallelism(int)— ограничение потоковWithCancellation(ct)— поддержка отменыWithMergeOptions(ParallelMergeOptions)—AutoBuffered(по умолчанию),FullyBuffered,NotBuffered
Пример:
var result = data
.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Where(x => ExpensiveFilter(x))
.Select(x => HeavyComputation(x))
.ToArray(); // барьер синхронизации
⚠️ Ловушки:
- Параллельная агрегация требует ассоциативной операции (например,
+,*,Min,Max— да;Average— нет, нужен(count, sum)).- Side effects в
Select/Where→ гонки.- Маленькие наборы (
<1000) — накладные расходы на потоки перевешивают выгоду.
📌 Приложение: Сводная таблица всех методов LINQ (.NET 8)
Всего 56 стандартных методов в
System.Linq.Enumerable(не считая перегрузок). Ниже — классификация.
| Категория | Методы (без перегрузок) | Кол-во | Примечание |
|---|---|---|---|
| Фильтрация | Where, OfType, Cast, Take, Skip, TakeWhile, SkipWhile, TakeLast, SkipLast | 9 | TakeLast/SkipLast — .NET 6+ |
| Проекция | Select, SelectMany | 2 | — |
| Сортировка | OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse | 5 | Reverse — не стабильный |
| Множества | Distinct, Union, Intersect, Except, Concat, Append, Prepend, DistinctBy, UnionBy, IntersectBy, ExceptBy | 11 | *By — .NET 6+/7+ |
| Агрегация | Count, LongCount, Any, All, Min, Max, MinBy, MaxBy, Sum, Average, Aggregate | 11 | MinBy/MaxBy — .NET 6+ |
| Группировка | GroupBy, ToLookup | 2 | — |
| Соединения | Join, GroupJoin | 2 | — |
| Элементы | First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault, DefaultIfEmpty | 9 | — |
| Генерация | Empty, Range, Repeat, Zip, Chunk | 5 | Chunk — .NET 6+ |